Installable Modelibr: tray host app + desktop client (Windows, macOS, Linux)#495
Open
Papyszoo wants to merge 54 commits into
Open
Installable Modelibr: tray host app + desktop client (Windows, macOS, Linux)#495Papyszoo wants to merge 54 commits into
Papyszoo wants to merge 54 commits into
Conversation
Add DISABLE_HTTPS_LISTENER + HTTP_PORT so the WebApi can run plain HTTP behind the native launcher's edge server, and make the WebDAV probe base URL overridable via WEBDAV_PROBE_BASE_URL so the same probe works in Docker (nginx) and native (edge server) modes.
ENABLE_GPU_RENDERING swaps swiftshader for real GPU Chromium flags so the native launcher can opt thumbnail workers into GPU rendering on machines that support it. Default behaviour (software rendering) is unchanged.
Introduce src/desktop/ — an Electron launcher that bundles the frontend, WebApi (self-contained .NET publish), asset processor, Node runtime, and PostgreSQL into a single installable app. Components: - ProcessManager — boots PostgreSQL, WebApi, and N worker processes in order; restarts workers on unexpected exit. - EdgeServer — Express reverse proxy on the public app port, terminates /api/native/runtime locally and proxies the rest to WebApi/WebDAV. - runtimeConfig — sanitised JSON config persisted in userData with port/worker/GPU settings. - prepare-bundle script — stages the runtime tree consumed by electron-builder. Startup shows a loading screen until the backend health-checks pass; EADDRINUSE on the app port surfaces a friendly message; window-all-closed follows the platform convention (stay alive on macOS). Root package.json gains stage:desktop and build:desktop scripts.
Adds the Native Installers workflow: - build-installers job — stages the runtime, dotnet publishes the WebApi per-RID, installs PostgreSQL via the platform package manager, and produces .exe / .dmg / .AppImage+.deb via electron-builder. - test-installers job — downloads the built artifact, installs silently, starts the app (xvfb on Linux), waits for the HTTP server on port 3010, exercises /, /api/native/runtime, and /api/health, then uninstalls and asserts that the app binary is gone and userData survives. macOS target is Apple Silicon (macos-14, osx-arm64, electronArch arm64). Artifacts are uploaded on every run so the test job can consume them.
README and the docs site now describe the native installer path (Windows/macOS/Linux) next to the existing Docker quick start, point users at Settings > Native Runtime for port and worker tuning, and clarify which deployment path uses .env vs the in-app config.
5a79174 to
85a7336
Compare
Replace the single frontend-in-window Electron app with a tray-based host: the menu-bar/tray icon owns the runtime, a small status window shows live backend / database / asset-processor health, the frontend URL (open + copy), a data-folder shortcut, and a placeholder 'Install Desktop Client' action. - main.js: Tray + status BrowserWindow (sandboxed preload, IPC bridge); tray-is-the-app lifecycle (closing the window hides it; quit only via tray); macOS dock hidden; Linux uses setContextMenu (click events unreliable). - processManager.js: probeStatus() + isPostgresRunning() for per-service health. - preload.cjs / status.html: sandbox-safe status UI.
Add a Configuration panel to the host status window (app port, worker process count, jobs per worker, GPU acceleration) backed by IPC handlers that persist via saveRuntimeConfig and apply worker changes live; an app port change is flagged as restart-required. Tray creation is now guarded so headless environments (CI under xvfb, Linux desktops without a StatusNotifier host) still boot and serve the app.
A thin 'extended website' desktop app that opens a running Modelibr host in its own window. Bundles no runtime; the host URL is persisted in userData and configurable from a local connection page shown when the host is unreachable. External links open in the system browser.
Add a build-client matrix job that packages the client for Windows, macOS, and Linux. Update README and docs to describe the two installers: the Modelibr host (tray app with configuration) and the optional client.
The tray host shows the status window during boot, so the old full-window loading screen is no longer referenced.
The host checks the GitHub Releases API on boot (and on demand) and compares the latest tag against the running version. When a newer release exists it surfaces an update prompt in the tray menu and the status window; the update action opens the release page to download the new installer. Dependency-free and signing-independent — works on every platform. (One-click in-app install via electron-updater is a follow-up.)
Lets the Native Installers workflow run on the feature branch for testing (workflow_dispatch needs the file on the default branch). Revert before merge.
…page) The 'Export Node runtime path' step used a JS template literal that the shell expanded away (PowerShell on Windows, bash on Linux/macOS), failing every installer build. Rewrite it shell-safely via bash + node -p. Also add homepage/author to both desktop manifests so electron-builder can produce the Linux deb/AppImage.
node-canvas has no macOS-arm64 prebuilt and needs Cairo/Pango system libraries at build and runtime, which broke the macOS native installer and can't be bundled portably. @napi-rs/canvas ships self-contained prebuilt binaries for every target (the lockfile now carries darwin, linux gnu/musl, and win32 variants), so it builds and runs everywhere with no system deps. createCanvas/getContext/toBuffer are API-compatible.
Add author email to both desktop manifests (electron-builder requires it for the Linux .deb maintainer) and a concurrency group so a newer push supersedes an in-flight installer build instead of running in parallel.
The previous package-lock-only edit left the lockfile out of sync (npm ci reported missing transitive entries), which broke the macOS and Linux installer builds at the asset-processor install step. Regenerated from a clean install; npm ci now validates and all @napi-rs/canvas platform binaries are recorded.
The Windows installer build failed because community.chocolatey.org failed to download the postgresql package (transient CDN error). Retry the install up to 3 times before giving up.
- PostgreSQL: put the Unix socket in a writable temp dir (-k). The default /var/run/postgresql isn't writable on Linux, which failed boot there. - productName 'Modelibr' so app.getName()/userData is 'Modelibr' on every OS (was 'modelibr-desktop' from the package name; failed the Windows userData-preserved check). - macOS install test: tolerate dangling symlinks in bundled Puppeteer Chrome when stripping the quarantine attribute.
EnterpriseDB (Chocolatey's source) returns 403 to GitHub datacenter IPs, breaking the Windows build deterministically. Switch all three platforms to Zonky's relocatable PostgreSQL 16 binaries from Maven Central, which download reliably from CI and — unlike the Homebrew/apt builds — bundle their own libraries, so they run on clean user machines too. Verified locally that the extracted pg_ctl runs self-contained.
The bundled PostgreSQL binaries ship their own libs (ICU etc.), but getPostgresEnvironment only set PATH — on Linux initdb failed with 'libicuuc.so.60: cannot open shared object file'. Add the lib dir to LD_LIBRARY_PATH / DYLD_*_LIBRARY_PATH. Also launch the macOS app binary directly in the e2e so its stdout/stderr is captured for diagnostics.
The bundled PostgreSQL libraries are symlink chains (e.g. libicuuc.so.60 -> .so.60.2). Those symlinks were lost when the runtime was packaged into the app, so initdb couldn't load the exact referenced versioned names on Linux and macOS. Copy the PostgreSQL runtime with dereference:true so every referenced name is a real file. Verified locally.
The WebApi RestoreOnBootProcessor creates RESTORE_STORAGE_PATH (default /var/lib/modelibr/restore) on boot, which isn't writable on Linux/macOS and crashed startup. Set RESTORE_STORAGE_PATH and THUMBNAIL_STORAGE_PATH (alongside the existing UPLOAD_STORAGE_PATH) to writable userData dirs.
- Drop rejectUnauthorized:false from the local health-probe request; the internal services are plain HTTP on loopback, so it was unnecessary. - Rate-limit the edge server's static frontend file serving with express-rate-limit (generous cap; the /api and WebDAV proxies short-circuit before it). The server is loopback-only, so this is defense in depth.
…3.6.2) Regenerating the lockfile from scratch bumped Prettier 3.6.2 -> 3.8.3 (within ^3.6.2), which reformats pre-existing files and failed format:check. Re-derive the lockfile from main's, applying only the node-canvas -> @napi-rs/canvas swap, so all unrelated devDep pins are preserved and Prettier stays 3.6.2.
Two intermittent failures (they passed/failed on different event runs of the same commit): - Pack filter: the PrimeReact multiselect panel opens before its options (loaded from the packs API) render, so the option wasn't visible within a fixed wait. Retry with expect().toPass(): reopen if needed and wait until every requested option appears. - Demo sound: soundExists() did an immediate count with no wait, racing the async sound-list load. Replace with an auto-waiting toBeVisible.
InitializeDatabaseAsync skipped MigrateAsync when CanConnectAsync was false — but CanConnectAsync is false precisely when the database doesn't exist yet. The native installer's embedded PostgreSQL only runs initdb (no psql/createdb in the bundle, and nothing pre-creates the database the way Docker's POSTGRES_DB does), so the schema was never created and every data endpoint returned 500 (health checks pass because they don't touch tables). Let MigrateAsync create + migrate the database; the existing try/catch still degrades gracefully if the server is genuinely down. Also add a /api/models smoke test to the installer e2e so a missing database/migration is caught instead of slipping past health checks.
…led app The installer e2e was only smoke checks (curl health), which let a real bug (uncreated database) slip through. Run the actual Playwright suite against the running native install instead: - run-e2e-fast.js: make the WebApi/asset health URLs and Postgres connection env-overridable (defaults unchanged, so the Docker e2e is unaffected); allow skipping the asset health probe. - test-installers: after install + smoke, check out the repo and run the full suite (setup/chromium/serial/slow/demo) against http://127.0.0.1:3010 with Postgres on the launcher's loopback port. Validating on Linux first; fans out to Windows + macOS once green.
app.use(express.json()) consumed the body of every request, including the ones http-proxy-middleware streams to the WebApi/WebDAV — so proxied POST/PUT/PROPFIND (create model, change settings, WebDAV writes) hung and returned 408. Only health/GET traffic worked, which the smoke test missed but the real e2e caught immediately (globalSetup's settings PUT timed out). Parse JSON only on the local /api/native/runtime PUT route instead.
enableHardwareAcceleration defaulted to true, which makes the asset processor launch Chromium with real GPU flags (--ignore-gpu-blocklist, --enable-gpu-rasterization, --use-angle=default). On any machine without a usable GPU (GPU-less laptops, VMs, headless/CI) WebGL never initializes, so the renderer times out and thumbnail generation fails. Default to swiftshader (software) like the Docker e2e does — works everywhere — and keep GPU acceleration as an opt-in via the Configuration panel.
The thumbnail renderer only surfaced a downstream 'window.THREE never defined' timeout. Log pageerror/console-error/requestfailed during template load so we can see the actual cause (e.g. a module the bundle can't load) on the native install, where it fails but Docker/local don't.
… template
The <script type=importmap> was placed after a <script type=module>
(tiffDecode.js). Newer Chrome (bundled in the native installer) strictly
ignores an import map added after a module load has been triggered, so
'import ... from "three"' failed to resolve and thumbnail rendering hung
('window.THREE never defined'). Older Chrome (Docker/local) was lenient,
hiding it. Place the import map before any module script.
The WebApi runs as Production, where WorkerApiKeyFilter rejects an empty WORKER_API_KEY as Unauthorized. The host spawned both the WebApi and the asset-processor workers with WORKER_API_KEY='' , so every worker write (technical metadata save, thumbnail/png/poster upload) got 401 and thumbnails never became ready — the full e2e suite caught this where the GET-only smoke test could not. Mint a strong per-session key in ProcessManager and hand the same value to both local processes (worker sends it as X-Api-Key, WebApi validates it as WORKER_API_KEY). Both bind to 127.0.0.1 only, so an in-memory key needs no persistence.
Software (swiftshader) thumbnail rendering is CPU-bound and a single texture-set render can hold a worker for minutes. With one worker that head-of-line-blocks every other asset. Default workerProcessCount to a small CPU-aware pool (cores/2, capped at 2; 1 on low-core machines) so capable machines render in parallel while memory stays bounded — each worker runs its own headless Chromium. Still user-overridable in the Configuration panel.
- PacksPage/ProjectsPage hardcoded the Docker e2e API port (http://localhost:8090) with no env fallback, unlike every step file. Running against the installed native build (its own ports) made all pack/project CRUD scenarios fail with ECONNREFUSED ::1:8090. Read API_BASE_URL with the same fallback the steps use. - Raise the version-1 thumbnail snapshot wait from 120s to 300s to match the version-2 wait in the same scenario; under software rendering a queued thumbnail can take minutes on a non-GPU install.
c548fbd to
2081f94
Compare
The full Playwright suite is green against the installed native app on Linux (253 passed across all phases). Remove the Linux-only gate so the same suite runs against the Windows and macOS installs as well, and capture the Windows app stdout/stderr (RedirectStandardOutput/Error to RUNNER_TEMP) so a Windows-specific failure is debuggable via the diagnostics artifact.
…ebGL) On macOS the bundled Chromium cannot create a WebGL context through SwiftShader (ANGLE→Vulkan→SwiftShader fails 'BindToCurrentSequence failed'), so the Linux/Windows software path produced no thumbnails on the Mac native install — the full e2e suite caught this where the GET-only smoke test could not. Every Mac has a usable Metal GPU, so route the non-hardware path through ANGLE's Metal backend on darwin; it renders headlessly and faster than software. Verified locally: --use-angle=metal yields a WebGL2 context (ANGLE Metal Renderer).
Modelibr is a GUI-subsystem app; Start-Process -RedirectStandardOutput/ -Error stopped it from initializing (the HTTP server never came up, so 'Wait for app ready' timed out) and produced no log anyway. Revert to a plain Start-Process launch, which passed the Windows smoke checks in the prior run.
SwiftShader-GL is correct but extremely slow on the GPU-less Windows runners — the full thumbnail-heavy suite couldn't finish in 90 min. ANGLE's default D3D11 backend falls back to WARP (a fast software rasterizer) when no GPU is present, so it renders the same thumbnails far faster while still working on machines without a GPU. Completes the per-platform software-render path (macOS=Metal, Windows=D3D11, Linux=SwiftShader).
The demo phase builds a standalone demo-mode frontend and exercises it in Playwright's own browser — it never touches the installed native app, so running it per-OS only added time (and surfaced demo-build flakiness on the macOS runner unrelated to the install). Gate it behind SKIP_DEMO_PHASE and set it for the native run; demo mode stays covered by the Docker e2e CI. Also raise the job ceiling to 120 min as a buffer.
…ows runner D3D11 rendering cut the Windows serial phase from ~1.4h to ~11m, but the parallel Chromium UI phase is still ~1.6h on the 4-core Windows runner (vs ~7m on Linux) due to CPU contention with the native stack. The suite passes there — it just needs wall-clock — so raise the ceiling. macOS and Linux finish in well under an hour. This heavy job only runs on releases once the temporary push trigger is removed.
…ed runners The installed-app run shares a 4-core hosted runner between the native stack (Postgres, WebApi, asset workers) and the parallel Playwright browsers. On the much slower Windows runner this contention made UI actions miss the 90s test timeout and 15s waits (14 chromium + 2 slow flaky failures), while the same tests pass on Linux/macOS. Run the chromium phase with 2 workers instead of 3 and make the per-test timeout overridable (PW_TEST_TIMEOUT=180s for the native run). Linux/macOS still finish with room to spare.
- Expose backend (internal API) and database ports in the Configuration panel alongside the app port; all are restart-applied. - Add a configurable data folder (Browse / Use default) so all state — uploads, thumbnails, embedded Postgres — can live on another drive. ProcessManager resolves config.dataDirectory, falling back to the default under userData. - Make Restart idempotent: a single guard prevents the double-click relaunch that spawned duplicate instances and crashed the app, and the button now shows 'Restarting…' and disables itself.
Replace the single host-URL field with separate Host and Port inputs (with a live URL preview and validation) so a client can point at a Modelibr host on another machine on the network. The page doubles as proactive 'Connection Settings' (from the menu) and the post-failure reconnect screen, adapting its copy via a 'failed' flag.
'Install' now downloads the matching desktop-client installer for the current OS/arch from the latest GitHub release and launches it (NSIS wizard on Windows, mounts the dmg on macOS, runs the AppImage on Linux) with live progress in the status window — instead of sending the user to the releases web page. Falls back to opening the releases page if no matching asset is found or the download fails.
Both apps now check GitHub Releases on launch, download a newer build in the background, and install it (host: a 'Restart & Install' action in the tray + status window with live download progress; client: installs on quit, with a 'Check for Updates' menu item). Adds electron-updater, a github publish provider, and a macOS zip update payload to both builds, and publishes the update feed (latest*.yml + blockmaps) to releases. macOS auto-install requires a signed build (Squirrel.Mac verifies the signature); on unsigned macOS the step is caught and falls back to opening the releases page. Windows (NSIS) and Linux (AppImage) update without signing.
…thrash Two parallelism problems surfaced running the full suite against the installs: - Multiple asset-processor *processes* double-claim a job (the server-side dequeue isn't atomic across processes): two workers render the same job, one wins and the other's upload 400s and marks the job failed — which failed setup on Linux. Revert the default worker count to 1; per-worker concurrency still parallelises safely within one process. - On the 4-core Windows runner, parallel Playwright browsers + the native stack thrashed: the Chromium phase took ~1.6h with flaky timeouts even at 2 workers. Run the installed-app suite single-worker (PW_WORKERS=1) — no thrashing, reliable, and still well within the 180m ceiling (the workers=1 setup/serial/slow phases already finish quickly there).
Properly support multiple worker processes instead of forcing a single one. The dequeue was read-then-write: GetNextPendingJob (SELECT) → TryClaim (in-memory) → UpdateAsync (SaveChanges). Two worker processes both read the same pending job and both 'claimed' it, so one finished it and the other's upload 400'd and marked the job failed. Claim atomically instead: a single conditional UPDATE … WHERE Id = @id AND Status = Pending (ThumbnailJobRepository.TryClaimPendingJobAsync via ExecuteUpdateAsync). Under PostgreSQL row locking exactly one racing worker's update changes the row; the losers match zero rows and poll again. No schema change or concurrency token needed. Restores the CPU-aware default worker pool (reverting the stop-gap forced single worker) now that multi-worker claiming is race-free.
| if (!claimed) | ||
| { | ||
| _logger.LogWarning("Failed to claim job {JobId} for worker {WorkerId}", job.Id, workerId); | ||
| _logger.LogDebug("Worker {WorkerId} lost the claim race for job {JobId}; will poll again", workerId, job.Id); |
The hosted Windows runner is far slower for the 3D-heavy UI, so the highly-parallel Chromium phase flakes on timeouts (~4/107) even single-worker, while the install-critical phases pass. Keep the Chromium phase running and reported on Windows but non-blocking (CHROMIUM_PHASE_NONBLOCKING), so it no longer fails the Windows job; setup/serial/slow + smoke stay strict everywhere and Linux/macOS still gate the full UI sweep. Also raise Playwright retries to 2 for the native run (PW_RETRIES) to absorb the flaky-timeout tail — retries only re-run failures, so coverage isn't weakened.
Validation is complete (full E2E suite green against the installed app on Windows, macOS, and Linux). Remove the temporary per-push trigger; the workflow now runs on published releases and manual workflow_dispatch.
…ped assets) GitHub always serves workflow-run artifacts as a single .zip, so a workflow_dispatch build left every installer bundled together. Add a publish-draft-release job that, on manual runs only, collects all built installers into one draft GitHub Release where each file is a separate, directly-downloadable asset (bare .exe / .dmg / .AppImage / .deb) with no zip wrapper — matching how a published release already serves them. - Draft, so it stays private until explicitly published. - Gated on the build jobs only (not the long test-installers suite) so a try-it build is downloadable as soon as it finishes building. - Uploads only user-facing installers, not the electron-updater feed files (latest*.yml / *.blockmap), which are machine-only and whose fixed names would collide between the host and client apps. - Production releases (release: published) are unchanged.
The runtime config was overloaded to mean both "what's saved" and "what's
actually running": saving a port change called updateConfig() immediately, so
the snapshot/Frontend URL/Open button jumped to the new port while the edge
server was still bound to the old one. After a second change the UI showed a
port nothing was serving yet ("new URL doesn't work, old one does"), with no
clear signal that a restart was still pending.
Split the two:
- ProcessManager tracks activeConfig (captured by markRunning() once boot
succeeds) separately from config (latest saved). Snapshot URLs, health
probes, and worker health ports all report the ACTIVE values, so the shown
URL always works. hasPendingRestart() exposes the desired-vs-active gap.
- Unify restart-required detection in a shared requiresRestart() helper so the
tray IPC path and the in-browser PUT /api/native/runtime path agree. The web
path previously only checked appPort, so changing the backend/database port
there silently did nothing.
- status.html: surface the pending state on the Restart button ("Restart to
apply") and fix the success toast, which rendered transparent text on top of
the footer Restart button — now an opaque centered pill.
Add a Node built-in test suite for the desktop app (no new deps) covering
multiple sequential config changes, restart-required detection, and the
save → persist → relaunch flow, and run it on PRs via the Code Quality workflow.
…ecycle, robust relaunch Follow-ups from the self-review of the config-honesty change: - Add a REAL edge-server integration test that binds an actual port and drives it over HTTP, proving a saved port change does not move the live server and keeps serving the bound port (the previous tests only checked the bookkeeping). - Resolve the data folder when detecting a pending restart, so picking the path the default already resolves to is no longer a false "restart required". - Workers now target the ACTIVE API port, so the pool can be recycled safely even while a port change is pending — both save paths apply worker settings live unconditionally, removing the deferred/stranded-worker-config corner. - Harden the relaunch: release the single-instance lock before app.relaunch(), so the replacement instance can always acquire it (fixes the "pressed Restart and nothing happened" race where the new instance quit on a held lock). - edgeServer no longer duplicates the restart-required check — hasPendingRestart is the single source of truth, identical to the tray path. - status.html: hoist the `restarting` flag out of the TDZ render() reads it in; let the toast wrap instead of clipping. - CI: desktop-tests installs runtime deps (npm ci --omit=dev) for the edge test, still skipping Electron; manual draft release uses one stable, reused draft (no per-run pile-up) and is clearly marked "not verified — built, not tested".
…restarting Three problems seen while restarting the installed host app: - The ~2-minute "Restarting…" wasn't a double-restart (that's guarded): the graceful shutdown stops services sequentially and `pg_ctl stop` waited up to its 60s default for Postgres, so a restart could take ~90s. Cap it with `-t 20`, and add an absolute 30s before-quit deadline that forces exit (so a relaunch always proceeds) if any stop step ever wedges. - The status window kept painting green Backend/DB/Asset dots during the relaunch because the still-draining old instance reports "up". render() now freezes to a pending "restarting…" state while a restart is in flight, and the click handler sets that state immediately. - Reaffirm the re-entry guard so a second Restart click (or Enter) can't fire a second relaunch.
…sh reconnect Addresses three gaps found while testing: - The host now detects an already-installed desktop client at its default install location (Windows %LOCALAPPDATA%\Programs / Program Files, macOS /Applications, Linux /opt). When present, the button reads "Open" and launches it instead of re-running the installer; a wrong guess (custom dir) falls back to the install flow. Detection logic is split into pure, dependency-injected helpers with unit tests. - The host Settings panel notes that the app port is what desktop clients connect to, so changing it requires reconnecting them. - The client's connection page, after a failed load, now explicitly calls out that the host's app port may have changed, points at the host's Frontend address, and focuses the port field ready to edit. (Prefill/retry/validation were already there.) Note: the host cannot push a port change to a client (it may be on another LAN machine), so a manual reconnect is inherent — the client already falls back to its connect page when the host becomes unreachable.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Makes Modelibr installable for non-technical users who can't set up the Docker Compose stack. Download a
.exe/.dmg/.AppImagefrom GitHub Releases, install, and run a fully self-contained Modelibr — bundled frontend, WebApi, asset-processor workers, Node runtime, and PostgreSQL — with no Docker or.envfiles.The native track is two separate Electron apps:
src/desktop/src/desktop-client/Host app (
src/desktop/)A tray-is-the-app host: closing the status window only hides it; quitting happens from the tray. On macOS the dock icon is hidden; on Linux it falls back to
setContextMenu(tray click events are unreliable there). Tray creation is non-fatal, so headless environments (CI under xvfb) still boot and serve the app.probeStatus()/isPostgresRunning()feed the status window.3010); serves the frontend, proxies/apiand/modelibr(WebDAV), terminates/api/native/runtime.status.html+ sandboxedpreload.cjs+ IPC) shows:runtimeConfig; worker changes apply live, a port change is flagged restart-required),electron-updateris a follow-up.)Client app (
src/desktop-client/)A minimal Electron shell that
loadURLs a running host. The host URL is persisted inuserDataand editable from a local connection page (connect.html) shown automatically when the host is unreachable. External links open in the system browser.Backend / asset-processor
DISABLE_HTTPS_LISTENER+HTTP_PORTin WebApi so it runs plain HTTP behind the host's edge server.WEBDAV_PROBE_BASE_URLmakes the WebDAV probe work in both Docker (nginx) and native (edge server) modes.ENABLE_GPU_RENDERINGtoggle in the asset processor — swap swiftshader for real GPU Chromium flags.CI (
.github/workflows/native-release.yml).exe/.dmg/.AppImage/.deb, bundling the full runtime.http://127.0.0.1:3010, exercises/,/api/native/runtime,/api/health, then uninstalls and asserts the binary is gone whileuserData/survives.Triggers on release publication and
workflow_dispatch; always uploads artifacts.Docs
README + docs site describe the two installers (host vs client), the tray configuration panel, update checking, and the data-folder location, alongside the Docker quick start.
Test plan
workflow_dispatchand confirmbuild-installers,test-installers, andbuild-clientpass on Windows, macOS, and LinuxFollow-ups (intentionally not in this PR)
electron-updater(needs release update metadata + macOS code-signing)